package com.jellyfishumbrella;

import static org.lwjgl.opengl.GL11.GL_LINE_LOOP;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_FAN;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Random;

import megamu.mesh.Corner;
import megamu.mesh.Edge;
import megamu.mesh.Region;
import megamu.mesh.Site;
import megamu.mesh.Voronoi;
import megamu.mesh.VoronoiInput;

import com.jellyfishumbrella.GL.GLColor;
import com.jellyfishumbrella.GL.MeshGL;
import com.jellyfishumbrella.GL.OctreeNode;
import com.jellyfishumbrella.GL.PolygonGroup;
import com.jellyfishumbrella.GL.Renderable;
import com.jellyfishumbrella.GL.ThreadLoop;
import com.jellyfishumbrella.GL.Vector2d;
import com.jellyfishumbrella.GL.Vector3d;
import com.jellyfishumbrella.GL.Vertex;
import com.jellyfishumbrella.noise.PerlinNoise;
import com.jellyfishumbrella.util.Trace;

public class VorLand2D extends MeshGL {
	protected final Random				R					= new Random();
	public final double					TOO_CLOSE			= 30;
	protected final int					NUM_BORDER_POINTS	= 25;
	protected final int					NUM_TERRAIN_POINTS	= 1000;
	protected final double				OUTER_RADIUS		= 1500;
	protected final double				INNER_RADIUS		= 750;
	protected final double				TERRAIN_BOUNDS		= 1000;

	public GameLogic					logic;

	protected double					twoPI				= Math.PI * 2;
	protected double					angleStep			= twoPI / (double) NUM_BORDER_POINTS;
	VoronoiInput<Site>					sites				= new VoronoiInput<Site>();
	protected Voronoi					voronoi;

	public LinkedHashMap<Site, Region>	allRegions;
	public LinkedHashMap<Site, Region>	landRegions;
	public LinkedHashMap<Site, Region>	waterRegions;

	protected double					minAlt				= 0;
	protected double					maxAlt				= 0;

	public boolean						READY				= false;

	public VorLand2D() {
		super();
	}

	public VorLand2D(GameLogic logic) {
		super();
		this.logic = logic;

		VorLand2DThread vorLand2DThread = new VorLand2DThread(logic);
		vorLand2DThread.start();
	}

	public class VorLand2DThread extends ThreadLoop {
		GameLogic	logic;
		VorLand2D	vorLand2D;

		public VorLand2DThread(GameLogic logic) {
			this.logic = logic;
			logic.allThreads.add(this);
		}

		public void run() {
			glMode = GL_LINE_LOOP;

			synchronized (polyGroups) {
				polyGroups = new HashSet<PolygonGroup>();
			}
			voronoi = new Voronoi();

			makeBorderPoints();
			makeTerrainPoints();

			voronoi.generate(sites);
			voronoi.relax(5);

			voronoi.populateSiteLinks();

			voronoi.populateRegionNeighbors();

			makeLandOrWater();
			expandBorder(1);

			divideLandAndWater(voronoi.getRegions());

			//Vector2d altBounds = applyElevations(voronoi);
			//minAlt = altBounds.x;
			//maxAlt = altBounds.y;
			maxAlt = applyElevations(voronoi);
			minAlt = applyBathymetry(voronoi);

			double scaler = 100.0 / (maxAlt - minAlt);
			System.out.println(scaler);
			scaleAndColorElevations(scaler);

			glMode = GL_TRIANGLE_FAN;
			makePolygons(voronoi.getRegions());

			createOctree();

			READY = true;
		}
	}

	public void createOctree() {
		Vector3d ltf = new Vector3d(-OUTER_RADIUS * 2, this.ltf.y, -OUTER_RADIUS * 2);//this.ltf;
		Vector3d rbb = new Vector3d(OUTER_RADIUS * 2, this.rbb.y, OUTER_RADIUS * 2);//this.rbb;

		ltf.add(new Vector3d(-100, -100, -100));
		rbb.add(new Vector3d(100, 100, 100));

		logic.octree = new OctreeNode<PolygonGroup>(null, ltf, rbb);
		synchronized (polyGroups) {
			for (PolygonGroup currPolyGroup : polyGroups) {
				logic.octree.insert(currPolyGroup);
			}
		}

		logic.octree.isComplete = true;
	}

	public void makeBorderPoints() {
		Trace.getCurrentMethodName();
		Site newSite;
		for (int i = 0; i < NUM_BORDER_POINTS; i++) {
			double angle = -i * angleStep;
			double X1 = OUTER_RADIUS * Math.cos(angle);
			double Z1 = OUTER_RADIUS * Math.sin(angle);
			newSite = new Site(X1, Z1);
			newSite.isBorder = true;
			sites.add(newSite);
			// this.addSinglePointPolygon(newSite);

			double X2 = INNER_RADIUS * Math.cos(angle);
			double Z2 = INNER_RADIUS * Math.sin(angle);
			newSite = new Site(X2, Z2);
			newSite.isBorder = true;
			sites.add(newSite);
			// this.addSinglePointPolygon(newSite);
		}
		this.refreshVerts = true;
		this.refreshIndices = true;
	}

	public void makeTerrainPoints() {
		Trace.getCurrentMethodName();
		boolean addMe;
		int currPoints = 0;
		// for (int i = 0; i < NUM_TERRAIN_POINTS; i++) {
		while (currPoints < NUM_TERRAIN_POINTS) {
			addMe = true;
			double X = TERRAIN_BOUNDS * (R.nextDouble() - 0.5);
			double Z = TERRAIN_BOUNDS * (R.nextDouble() - 0.5);

			for (int j = 0; j < sites.size(); j++) {
				double dx = X - ((Site) sites.get(j)).x;
				double dz = Z - ((Site) sites.get(j)).z;
				double dist = Math.sqrt(dx * dx + dz * dz);

				//if (dist < TOO_CLOSE) {
				//	addMe = false;
				//	break;
				//}
			}
			if (addMe) {
				currPoints++;
				Site newSite = new Site(X, Z);
				newSite.isBorder = false;
				sites.add(newSite);
				// this.addSinglePointPolygon(new Vertex(X, 0, Z));
			}
		}

		this.refreshVerts = true;
		this.refreshIndices = true;
	}

	public void makeLandOrWater() {
		Trace.getCurrentMethodName();
		double minNoise = Double.MAX_VALUE;
		double maxNoise = Double.MIN_VALUE;

		double nScale = .005;

		for (int i = 0; i < sites.size(); i++) {
			Site currSite = (Site) sites.get(i);
			double Y = R.nextDouble() / 100.0;
			System.out.println("-----------------------------" + Y);
			double noise = PerlinNoise.value(nScale * currSite.x, Y, nScale * currSite.z);

			if (noise > 0.25) {
				currSite.isOcean = true;
			}

			minNoise = Math.min(minNoise, noise);
			maxNoise = Math.max(maxNoise, noise);
		}
	}

	public void expandBorder(int iterations) {
		Trace.getCurrentMethodName();
		LinkedHashMap<String, Corner> unchecked;

		for (int i = 0; i < iterations; i++) {
			unchecked = new LinkedHashMap<String, Corner>(voronoi.corners);

			for (Map.Entry<String, Corner> entry : voronoi.corners.entrySet()) {
				Corner currCorner = (Corner) entry.getValue();
				if (currCorner.isBorder) {
					for (Corner currNeighbor : currCorner.neighbors) {
						currNeighbor.isBorder = true;
						unchecked.remove(currNeighbor);
					}
				}
			}
		}
	}

	public void uncheckAllSites() {
		Trace.getCurrentMethodName();
		for (int i = 0; i < sites.size(); i++) {
			Site currSite = (Site) sites.get(i);
			currSite.checked = false;
		}
	}

	public void divideLandAndWater(Region[] regions) {
		Trace.getCurrentMethodName();
		uncheckAllSites();

		allRegions = new LinkedHashMap<Site, Region>();
		landRegions = new LinkedHashMap<Site, Region>();
		waterRegions = new LinkedHashMap<Site, Region>();

		for (int i = 0; i < sites.size(); i++) {
			Site currSite = (Site) sites.get(i);
			currSite.checked = true;

			if (currSite.isBorder) {
				makeSelfAndWetNeighborsOcean(currSite);
				// assumes cells are all marked unchecked...
				// return;
				break;
			}
		}

		for (int i = 0; i < regions.length; i++) {
			Region currRegion = regions[i];
			allRegions.put(currRegion.site, currRegion);

			ArrayList<Corner> corners = currRegion.getCorners();

			if (currRegion.site.isWater()) {
				waterRegions.put(currRegion.site, currRegion);
				for (Corner currCorner : corners) {
					currCorner.isWater = true;
				}
			} else {
				landRegions.put(currRegion.site, currRegion);
				for (Corner currCorner : corners) {
					if (currCorner.isWater) {
						currCorner.isCoast = true;
					}
					// currCorner.isWater = false;
				}
			}
		}
	}

	public void makeSelfAndWetNeighborsOcean(Site currSite) {
		Trace.getCurrentMethodName();
		currSite.isOcean = true;

		Site nextSite = currSite.nextUncheckedSite();

		while (nextSite != null) {
			nextSite.checked = true;
			if (nextSite.isWater()) {
				makeSelfAndWetNeighborsOcean(nextSite);
			}
			nextSite = currSite.nextUncheckedSite();
		}
	}

	public void optimizeWater(Voronoi voronoi) {
		VoronoiInput keepUs = new VoronoiInput();

		for (Region currRegion : voronoi.getRegions()) {
			if (currRegion.site.isWater()) {
				boolean surroundedByWater = true;
				for (Site neighbor : currRegion.site.links) {
					if (!neighbor.isWater()) {
						surroundedByWater = false;
						break;
					}
				}
				if (!surroundedByWater) {
					keepUs.add(currRegion.site);
				}
			} else {
				keepUs.add(currRegion.site);
			}
		}

		sites = keepUs;
	}

	public double applyElevations(Voronoi voronoi) {
		Trace.getCurrentMethodName();
		Region[] regions = voronoi.getRegions();
		ArrayList<Edge> edges = voronoi.getEdges();

		Region firstRegion = null;

		for (int i = 0; i < regions.length; i++) {
			Region currRegion = regions[i];
			Site currSite = regions[i].site;
			if (currSite.isBorder) {
				setAllEdgeAlts(currRegion, 0.0f);
				if (firstRegion == null) {
					firstRegion = currRegion;
				}
			} else if (currSite.isWater()) {
				setAllEdgeAlts(currRegion, 0);
			} else {
				setAllEdgeAlts(currRegion, Double.MAX_VALUE);
			}
		}

		int numChanged = 1;
		double currMaxAlt = 0;
		double currMinAlt = 0;

		while (numChanged != 0) {
			numChanged = 0;
			for (Region currRegion : regions) {
				ArrayList<Corner> corners = currRegion.getCorners();
				for (Corner currCorner : corners) {
					if (!currCorner.isBorder && (currCorner.y >= currMaxAlt || currCorner.y <= currMinAlt)) {
						for (Corner currNeighbor : currCorner.neighbors) {
							if (!currCorner.isWater) {
								if (currCorner.y > currMaxAlt && currCorner.y > currNeighbor.y) {
									currCorner.y = currNeighbor.y + 1;
									numChanged++;
								}
							} else {
								currCorner.y = 0;
							}
						}
					}
				}
			}
			currMaxAlt++;
			//currMinAlt--;
		}
		//return new Vector2d(currMinAlt, currMaxAlt);
		return currMaxAlt;
	}
	
	public double applyBathymetry(Voronoi voronoi){
		Trace.getCurrentMethodName();
		Region[] regions = voronoi.getRegions();
		ArrayList<Edge> edges = voronoi.getEdges();

		Region firstRegion = null;

		for (int i = 0; i < regions.length; i++) {
			Region currRegion = regions[i];
			Site currSite = regions[i].site;
			if (currSite.isBorder) {
				//setAllEdgeAlts(currRegion, 0.0f);
				if (firstRegion == null) {
					firstRegion = currRegion;
					setAllEdgeAlts(currRegion, Double.MIN_VALUE);
				}
			} else if (currSite.isWater()) {
				setAllEdgeAlts(currRegion, Double.MIN_VALUE);
			} else {
				//setAllEdgeAlts(currRegion, Double.MAX_VALUE);
			}
		}
		
		int numChanged = 1;
		double currMaxAlt = 0;
		double currMinAlt = 0;

		while (numChanged != 0) {
			numChanged = 0;
			for (Region currRegion : regions) {
				ArrayList<Corner> corners = currRegion.getCorners();
				for (Corner currCorner : corners) {
					//if (!currCorner.isBorder && (currCorner.y >= currMaxAlt || currCorner.y <= currMinAlt)) {
					if(currCorner.y <= currMinAlt){
						for (Corner currNeighbor : currCorner.neighbors) {
							if (currCorner.isWater) {
								if (currCorner.y < currMinAlt && currCorner.y < currNeighbor.y) {
									currCorner.y = currNeighbor.y - 1;
									numChanged++;
								}
							} else {
								//currCorner.y = 0;
							}
						}
					}
				}
			}
			//currMaxAlt++;
			currMinAlt--;
		}
		//return new Vector2d(currMinAlt, currMaxAlt);
		return currMinAlt;
	}

	public void scaleAndColorElevations(double scaleFactor) {
		Trace.getCurrentMethodName();
		double erosion = 0.0;// 175; // 0 = none, 0.175 = good, 1.0 = lots, >1.0 = waaaaayyyyy too much
		double adjMinAlt = (scaleFactor * minAlt);
		double adjMaxAlt = (scaleFactor * maxAlt);

		for (int i = 0; i < sites.size(); i++) {
			Site currSite = (Site) sites.get(i);

			Region currRegion = currSite.region;

			double avgAlt = 0;

			for (Corner currCorner : currRegion.getCorners()) {
				if (!currCorner.checked) {
					currCorner.checked = true;

					if (currCorner.isWater || currCorner.isBorder) {
						currCorner.glColor = new GLColor(0, 0.5, 1.0, 1.0);
					} else if (currCorner.isCoast) {
						currCorner.glColor = new GLColor(0.6, 0.5, 0.0, 1.0);
					} else {
						currCorner.glColor = new GLColor(0.5 * currCorner.y / (maxAlt), 0.5 - 0.5 * currCorner.y / (maxAlt), 0.5 * currCorner.y / (maxAlt), 1.0);
					}
					//if (currCorner.isCoast) {
					//	currCorner.uv = new Vector2d(R.nextDouble(), .075);
					//} else {
						currCorner.uv = new Vector2d(R.nextDouble(), (currCorner.y - minAlt) / (maxAlt - minAlt));
					//}
					currCorner.y = (scaleFactor * currCorner.y) / Math.exp(erosion * ((maxAlt) - currCorner.y))-1;
				}
				if (currCorner.checked) {
					avgAlt += currCorner.y;
				}
			}
			avgAlt /= (double) currRegion.vertCount();
			currRegion.site.y = avgAlt;
		}

		minAlt = 0;
		maxAlt = 0;

		for (int i = 0; i < sites.size(); i++) {
			Site currSite = (Site) sites.get(i);
			Region currRegion = currSite.region;
			ArrayList<Vertex> verts = currRegion.getVerts();

			float avg_Alt = 0;
			for (int j = 1; j < verts.size(); j++) {
				avg_Alt += verts.get(j).y;
				minAlt = (float) Math.min(minAlt, verts.get(j).y);
				maxAlt = (float) Math.max(maxAlt, verts.get(j).y);
			}

			avg_Alt /= (verts.size() - 1);
			verts.get(0).y = currSite.y = avg_Alt;
			// verts.get(0).uv = new Vector2d(verts.get(0).y/maxAlt,verts.get(0).y/maxAlt);
		}
	}

	public void setAllEdgeAlts(Region currRegion, double alt) {
		ArrayList<Corner> corners = currRegion.getCorners();

		for (Corner currCorner : corners) {
			if (!currCorner.isCoast){// && !currCorner.isBorder) {// && !currCorner.isWater) {
				currCorner.y = alt;
			} else {
				currCorner.y = 0;
			}
		}
		if (!currRegion.site.isBorder) {
			currRegion.site.y = alt;
		}
	}

	public void makePolygons(Region[] regions) {
		Trace.getCurrentMethodName();
		synchronized (polyGroups) {
			if (logic.renderGL.contains((Renderable) this)) {
				logic.renderGL.remove(this);
			}
			logic.renderGL.add(this);
			this.clear();

			textures.add(logic.renderGL.textureHash.get("test_ALT_GRAD"));
			textures.add(logic.renderGL.textureHash.get("testFractal_Noise"));

			System.out.println("There are " + regions.length + " regions.");
			for (Region currRegion : regions) {
				currRegion.parentMesh = this;
				polyGroups.add(currRegion);
				// currRegion.createGLpolygon(false);
				currRegion.setLOD(0);
			}

			this.refreshVerts = true;
			this.refreshIndices = true;
		}
	}

	public int getRegionCount() {
		if (allRegions != null) {
			return allRegions.size();
		} else {
			return 0;
		}
	}
}
